import os
import sys
from pathlib import Path
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import yaml
import copy
import json
import subprocess


APP_TITLE = "YAML 编辑器"
DEFAULT_DIR = Path(__file__).resolve().parent.parent / "public" / "config"
SUPPORTED_EXTS = (".yaml", ".yml")
RECENTS_FILE = Path.home() / ".yaml_editor_recent_dirs.json"


# 已移除模板功能


def ensure_classic_windows_theme():
    style = ttk.Style()
    # Try Windows classic themes in order
    for theme in ("vista", "xpnative", "winnative", "clam", "alt", "default"):
        try:
            style.theme_use(theme)
            break
        except tk.TclError:
            continue
    return style


class YamlEditorApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.geometry("1200x780")
        self.minsize(1000, 600)
        self.style = ensure_classic_windows_theme()

        # State
        self.current_file: Path | None = None
        self.data = None
        self.item_paths = {}  # tree item id -> list of path keys/indexes
        self.unsaved = False
        # Working directory (generic, not limited to project)
        self.working_dir: Path = self._initial_dir()
        self._all_files: list[str] = []
        self.recent_dirs: list[str] = self._load_recent_dirs()
        self._dir_signature: str | None = None
        self._watch_after_id: str | None = None

        self._build_menu()
        self._build_layout()
        self._bind_shortcuts()
        self._refresh_file_list()

    # UI building
    def _build_menu(self):
        menubar = tk.Menu(self)

        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="打开...", accelerator="Ctrl+O", command=self.open_file)
        file_menu.add_command(label="保存", accelerator="Ctrl+S", command=self.save_file)
        file_menu.add_command(label="另存为...", command=self.save_file_as)

        # 已移除：新建（模板）

        # Recent directories
        self.recent_menu = tk.Menu(file_menu, tearoff=0)
        file_menu.add_cascade(label="最近打开目录", menu=self.recent_menu)
        self._update_recent_menu()

        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self.quit)

        edit_menu = tk.Menu(menubar, tearoff=0)
        edit_menu.add_command(label="添加键/项", accelerator="Ctrl+N", command=self.add_item)
        edit_menu.add_command(label="删除选中", accelerator="Del", command=self.delete_item)

        tools_menu = tk.Menu(menubar, tearoff=0)
        tools_menu.add_command(label="验证 YAML", command=self.validate_yaml)
        tools_menu.add_command(label="格式化 YAML", command=self.format_yaml)

        help_menu = tk.Menu(menubar, tearoff=0)
        help_menu.add_command(label="关于", command=lambda: messagebox.showinfo(APP_TITLE, "YAML 编辑器\n\n简介：轻量级 YAML 图形编辑器。\n功能：Yaml文件递归检索，轻量化迅速编辑Yaml文件。\n版权：ConlZy版权所有 © 2025"))
        menubar.add_cascade(label="文件", menu=file_menu)
        menubar.add_cascade(label="编辑", menu=edit_menu)
        menubar.add_cascade(label="工具", menu=tools_menu)
        menubar.add_cascade(label="帮助", menu=help_menu)
        self.config(menu=menubar)

    def _build_layout(self):
        # Top-level: left file list + right notebook
        root_pane = ttk.Panedwindow(self, orient=tk.HORIZONTAL)
        root_pane.pack(fill=tk.BOTH, expand=True)

        # Left: file list
        left_frame = ttk.Frame(root_pane)
        ttk.Label(left_frame, text="工作目录", anchor="w").pack(fill=tk.X)
        self.dir_label = ttk.Label(left_frame, text=str(self.working_dir), foreground="#555", anchor="w")
        self.dir_label.pack(fill=tk.X)
        # File search
        search_file_bar = ttk.Frame(left_frame)
        search_file_bar.pack(fill=tk.X)
        ttk.Label(search_file_bar, text="搜索文件", anchor="w").pack(side=tk.LEFT, padx=4)
        self.file_search = ttk.Entry(search_file_bar)
        self.file_search.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4, pady=4)
        self.file_search.bind("<KeyRelease>", lambda e: self._filter_file_list())
        self.file_list = tk.Listbox(left_frame, height=8)
        self.file_list.pack(fill=tk.BOTH, expand=True)
        self.file_list.bind("<<ListboxSelect>>", self._on_file_select)
        self.file_list.bind("<Button-3>", self._on_file_list_right_click)
        left_buttons = ttk.Frame(left_frame)
        left_buttons.pack(fill=tk.X)
        ttk.Button(left_buttons, text="打开文件...", command=self.open_file).pack(side=tk.LEFT, padx=4, pady=4)
        ttk.Button(left_buttons, text="打开目录...", command=self.open_directory).pack(side=tk.LEFT, padx=4, pady=4)
        ttk.Button(left_buttons, text="刷新", command=self._refresh_file_list).pack(side=tk.LEFT, padx=4, pady=4)

        # Right: notebook with structure and raw tabs
        right_frame = ttk.Frame(root_pane)
        self.notebook = ttk.Notebook(right_frame)
        self.tab_struct = ttk.Frame(self.notebook)
        self.tab_raw = ttk.Frame(self.notebook)
        self.notebook.add(self.tab_struct, text="结构视图")
        self.notebook.add(self.tab_raw, text="原始YAML")
        self.notebook.pack(fill=tk.BOTH, expand=True)

        # Structure tab: tree + editor
        struct_pane = ttk.Panedwindow(self.tab_struct, orient=tk.HORIZONTAL)
        struct_pane.pack(fill=tk.BOTH, expand=True)

        tree_frame = ttk.Frame(struct_pane)
        # Tree search bar
        tree_search_bar = ttk.Frame(tree_frame)
        tree_search_bar.pack(fill=tk.X)
        ttk.Label(tree_search_bar, text="搜索结构", anchor="w").pack(side=tk.LEFT, padx=4)
        self.tree_search_entry = ttk.Entry(tree_search_bar)
        self.tree_search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=4, pady=4)
        self.tree_search_entry.bind("<Return>", lambda e: self._search_tree())
        ttk.Button(tree_search_bar, text="搜索", command=self._search_tree).pack(side=tk.LEFT, padx=4)
        ttk.Button(tree_search_bar, text="展开全部", command=self._expand_all).pack(side=tk.LEFT, padx=4)
        ttk.Button(tree_search_bar, text="收缩全部", command=self._collapse_all).pack(side=tk.LEFT, padx=4)
        # Treeview
        self.tree = ttk.Treeview(tree_frame, columns=("type", "value"), show="tree headings")
        self.tree.heading("type", text="类型")
        self.tree.heading("value", text="值")
        self.tree.column("type", width=120, anchor="w")
        self.tree.column("value", width=420, anchor="w")
        yscroll = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL, command=self.tree.yview)
        self.tree.configure(yscrollcommand=yscroll.set)
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        yscroll.pack(side=tk.RIGHT, fill=tk.Y)
        self.tree.bind("<<TreeviewSelect>>", self._on_tree_select)
        # Right-click context menu on tree
        self.tree.bind("<Button-3>", self._on_tree_right_click)

        editor_frame = ttk.Frame(struct_pane)
        ttk.Label(editor_frame, text="值编辑", anchor="w").pack(fill=tk.X)
        self.value_text = tk.Text(editor_frame, height=12, wrap="word")
        self.value_text.pack(fill=tk.BOTH, expand=True)
        editor_buttons = ttk.Frame(editor_frame)
        editor_buttons.pack(fill=tk.X)
        ttk.Button(editor_buttons, text="应用更改", command=self.apply_value_change).pack(side=tk.LEFT, padx=4, pady=4)
        ttk.Button(editor_buttons, text="添加键/项", command=self.add_item).pack(side=tk.LEFT, padx=4, pady=4)
        ttk.Button(editor_buttons, text="删除选中", command=self.delete_item).pack(side=tk.LEFT, padx=4, pady=4)

        struct_pane.add(tree_frame, weight=3)
        struct_pane.add(editor_frame, weight=2)

        # Raw tab: text area
        raw_frame = ttk.Frame(self.tab_raw)
        raw_frame.pack(fill=tk.BOTH, expand=True)
        self.raw_text = tk.Text(raw_frame, wrap="none")
        raw_ys = ttk.Scrollbar(raw_frame, orient=tk.VERTICAL, command=self.raw_text.yview)
        raw_xs = ttk.Scrollbar(raw_frame, orient=tk.HORIZONTAL, command=self.raw_text.xview)
        self.raw_text.configure(yscrollcommand=raw_ys.set, xscrollcommand=raw_xs.set)
        self.raw_text.pack(fill=tk.BOTH, expand=True)
        raw_ys.pack(side=tk.RIGHT, fill=tk.Y)
        raw_xs.pack(side=tk.BOTTOM, fill=tk.X)

        # Status bar
        self.status = ttk.Label(self, relief=tk.SUNKEN, anchor="w")
        self.status.pack(fill=tk.X)

        root_pane.add(left_frame, weight=1)
        root_pane.add(right_frame, weight=4)

    def _bind_shortcuts(self):
        self.bind_all("<Control-o>", lambda e: self.open_file())
        self.bind_all("<Control-s>", lambda e: self.save_file())
        self.bind_all("<Delete>", lambda e: self.delete_item())
        self.bind_all("<Control-n>", lambda e: self.add_item())

    # Data operations
    def _initial_dir(self) -> Path:
        # 默认使用程序当前工作目录
        try:
            return Path.cwd()
        except Exception:
            return Path.home()

    def _refresh_file_list(self):
        # 递归列出 YAML 文件并应用搜索过滤
        self.file_list.delete(0, tk.END)
        self._all_files = []
        if self.working_dir and self.working_dir.exists():
            self.dir_label.config(text=str(self.working_dir))
            paths = []
            try:
                paths += [p for p in self.working_dir.rglob("*.yaml") if p.is_file()]
                paths += [p for p in self.working_dir.rglob("*.yml") if p.is_file()]
            except Exception:
                paths = []
            # 去重并排序
            unique = sorted({str(p) for p in paths})
            self._all_files = unique
            self._filter_file_list()
            if self.file_list.size() == 0:
                self.file_list.insert(tk.END, "(目录内无 YAML 文件)")
        else:
            self.dir_label.config(text=f"未找到目录: {self.working_dir}")
            self.file_list.insert(tk.END, "(目录不可用)")

    def _filter_file_list(self):
        q = (self.file_search.get() if hasattr(self, "file_search") else "").strip().lower()
        self.file_list.delete(0, tk.END)
        for s in self._all_files:
            if not q or q in s.lower():
                self.file_list.insert(tk.END, s)
        if self.file_list.size() == 0:
            self.file_list.insert(tk.END, "(无匹配 YAML 文件)")

    def open_directory(self):
        path = filedialog.askdirectory(title="选择工作目录", initialdir=str(self.working_dir))
        if path:
            self.working_dir = Path(path)
            self._refresh_file_list()
            self._add_recent_dir(self.working_dir)
            self._update_recent_menu()
            self._start_watch_dir()

    def _on_file_select(self, event):
        sel = self.file_list.curselection()
        if not sel:
            return
        path = Path(self.file_list.get(sel[0]))
        if path.is_file():
            self.load_file(path)

    def open_file(self):
        initial = str(self.working_dir if self.working_dir and self.working_dir.exists() else (Path.cwd() if Path.cwd() else Path.home()))
        path = filedialog.askopenfilename(
            title="选择 YAML 文件",
            filetypes=[("YAML files", "*.yaml *.yml"), ("All files", "*.*")],
            initialdir=initial,
        )
        if path:
            self.load_file(Path(path))

    def load_file(self, path: Path):
        try:
            with open(path, "r", encoding="utf-8") as f:
                text = f.read()
            self.data = yaml.safe_load(text)
            self.current_file = path
            self.unsaved = False
            self._refresh_views()
            self._set_status(f"已加载: {path}")
        except Exception as e:
            messagebox.showerror("加载失败", f"无法解析 YAML:\n{e}")

    def save_file(self):
        if not self.current_file:
            return self.save_file_as()
        try:
            # backup
            bak = self.current_file.with_suffix(self.current_file.suffix + ".bak")
            if self.current_file.exists():
                try:
                    bak.write_text(self.current_file.read_text(encoding="utf-8"), encoding="utf-8")
                except Exception:
                    pass

            # if on RAW tab, take text directly
            if self.notebook.index(self.notebook.select()) == 1:
                txt = self.raw_text.get("1.0", tk.END)
                # validate before save
                yaml.safe_load(txt)
                self.current_file.write_text(txt, encoding="utf-8")
                self.data = yaml.safe_load(txt)
            else:
                txt = yaml.safe_dump(self.data, allow_unicode=True, sort_keys=False)
                self.current_file.write_text(txt, encoding="utf-8")
                self.raw_text.delete("1.0", tk.END)
                self.raw_text.insert("1.0", txt)
            self.unsaved = False
            self._set_status(f"已保存: {self.current_file}")
        except Exception as e:
            messagebox.showerror("保存失败", f"保存时出错:\n{e}")

    def save_file_as(self):
        path = filedialog.asksaveasfilename(
            title="另存为",
            defaultextension=".yaml",
            filetypes=[("YAML", "*.yaml"), ("YML", "*.yml"), ("All files", "*.*")],
        )
        if path:
            self.current_file = Path(path)
            self.save_file()

    # 已移除：新建模板功能

    # Structure view helpers
    def _refresh_views(self):
        # 记录展开、选择与滚动状态
        try:
            expanded_paths = set()
            for item_id in list(self.item_paths.keys()):
                if self.tree.exists(item_id) and self.tree.item(item_id, "open"):
                    expanded_paths.add(tuple(self.item_paths[item_id]))
            selected = self.tree.selection()
            selected_path = tuple(self.item_paths[selected[0]]) if selected else None
            yview_top = self.tree.yview()[0]
        except Exception:
            expanded_paths = set()
            selected_path = None
            yview_top = 0.0

        # Refresh tree
        for item in self.tree.get_children(""):
            self.tree.delete(item)
        self.item_paths.clear()

        def insert_node(parent, key, value, path_list):
            vtype = type(value).__name__
            display_key = str(key)
            display_val = self._short_value(value)
            node_id = self.tree.insert(parent, "end", text=display_key, values=(vtype, display_val))
            self.item_paths[node_id] = list(path_list)
            if isinstance(value, dict):
                for k, v in value.items():
                    insert_node(node_id, k, v, path_list + [k])
            elif isinstance(value, list):
                for idx, v in enumerate(value):
                    insert_node(node_id, idx, v, path_list + [idx])

        if isinstance(self.data, dict):
            for k, v in self.data.items():
                insert_node("", k, v, [k])
        elif isinstance(self.data, list):
            for idx, v in enumerate(self.data):
                insert_node("", idx, v, [idx])
        else:
            # scalar root
            insert_node("", "root", self.data, [])

        # Refresh raw
        try:
            txt = yaml.safe_dump(self.data, allow_unicode=True, sort_keys=False)
        except Exception:
            txt = "" if self.data is None else str(self.data)
        self.raw_text.delete("1.0", tk.END)
        self.raw_text.insert("1.0", txt)

        # update status
        if self.current_file:
            self._set_status(f"正在编辑: {self.current_file}")

        # 恢复展开、选择与滚动
        try:
            for item_id, path_list in self.item_paths.items():
                if tuple(path_list) in expanded_paths:
                    self.tree.item(item_id, open=True)
            if selected_path is not None:
                for item_id, path_list in self.item_paths.items():
                    if tuple(path_list) == selected_path:
                        self.tree.selection_set(item_id)
                        self.tree.see(item_id)
                        break
            self.tree.yview_moveto(yview_top)
        except Exception:
            pass

    def _expand_all(self):
        for iid in self.tree.get_children(""):
            self._set_open_recursive(iid, True)

    def _collapse_all(self):
        for iid in self.tree.get_children(""):
            self._set_open_recursive(iid, False)

    def _set_open_recursive(self, item_id, is_open: bool):
        try:
            self.tree.item(item_id, open=is_open)
            for child in self.tree.get_children(item_id):
                self._set_open_recursive(child, is_open)
        except Exception:
            pass

    def _short_value(self, value):
        s = str(value)
        return s if len(s) <= 120 else s[:117] + "..."

    def _on_tree_select(self, event):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0], [])
        value = self._get_value_by_path(path)
        # Show pretty YAML for value
        try:
            txt = yaml.safe_dump(value, allow_unicode=True, sort_keys=False)
        except Exception:
            txt = str(value)
        self.value_text.delete("1.0", tk.END)
        self.value_text.insert("1.0", txt)

    def _on_tree_right_click(self, event):
        row = self.tree.identify_row(event.y)
        if row:
            self.tree.selection_set(row)
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0], [])
        self._build_tree_context_menu(path)
        try:
            self.tree_menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.tree_menu.grab_release()

    def _build_tree_context_menu(self, path):
        self.tree_menu = tk.Menu(self, tearoff=0)
        parent = self._get_value_by_path(path[:-1]) if path else self.data
        key = path[-1] if path else None
        value = self._get_value_by_path(path)
        is_dict_entry = isinstance(parent, dict) and isinstance(key, str)
        is_list_entry = isinstance(parent, list) and isinstance(key, int)

        # Global controls
        self.tree_menu.add_command(label="展开全部", command=self._expand_all)
        self.tree_menu.add_command(label="收缩全部", command=self._collapse_all)
        self.tree_menu.add_separator()
        # Always available
        self.tree_menu.add_command(label="编辑值...", command=self.edit_value_quick)
        self.tree_menu.add_separator()
        self.tree_menu.add_command(label="添加键/项...", command=self.add_item)
        self.tree_menu.add_command(label="删除选中", command=self.delete_item)
        # Conditional
        self.tree_menu.add_command(
            label="重命名键...",
            command=self.rename_key,
            state=("normal" if is_dict_entry else "disabled"),
        )
        self.tree_menu.add_command(
            label="复制选中",
            command=self.duplicate_item,
            state=("normal" if (is_dict_entry or is_list_entry) else "disabled"),
        )
        self.tree_menu.add_command(
            label="上移",
            command=lambda: self.move_item(-1),
            state=("normal" if is_list_entry else "disabled"),
        )
        self.tree_menu.add_command(
            label="下移",
            command=lambda: self.move_item(1),
            state=("normal" if is_list_entry else "disabled"),
        )
        self.tree_menu.add_separator()
        self.tree_menu.add_command(label="复制路径", command=self.copy_path)

    # 文件列表右键菜单
    def _on_file_list_right_click(self, event):
        try:
            index = self.file_list.nearest(event.y)
            if index is not None:
                self.file_list.selection_clear(0, tk.END)
                self.file_list.selection_set(index)
            sel = self.file_list.curselection()
            path_str = self.file_list.get(sel[0]) if sel else None
        except Exception:
            path_str = None
        self._build_file_list_context_menu(path_str)
        try:
            self.file_list_menu.tk_popup(event.x_root, event.y_root)
        finally:
            self.file_list_menu.grab_release()

    def _build_file_list_context_menu(self, path_str: str | None):
        self.file_list_menu = tk.Menu(self, tearoff=0)
        is_valid = False
        p = None
        try:
            if path_str and not path_str.startswith("("):
                p = Path(path_str)
                is_valid = p.exists() and p.is_file()
        except Exception:
            is_valid = False

        self.file_list_menu.add_command(
            label="打开", command=(lambda: self.load_file(p)) if is_valid else None,
            state=("normal" if is_valid else "disabled")
        )
        self.file_list_menu.add_command(
            label="复制路径",
            command=(lambda: (self.clipboard_clear(), self.clipboard_append(path_str))) if path_str else None,
            state=("normal" if path_str else "disabled")
        )
        self.file_list_menu.add_command(
            label="打开所在目录",
            command=(lambda: self._open_in_explorer(p)) if is_valid else None,
            state=("normal" if is_valid else "disabled")
        )
        self.file_list_menu.add_separator()
        self.file_list_menu.add_command(label="刷新", command=self._refresh_file_list)

    def _open_in_explorer(self, p: Path):
        try:
            # 打开父目录（Windows）
            os.startfile(str(p.parent))
        except Exception:
            pass

    def _search_tree(self):
        q = (self.tree_search_entry.get() if hasattr(self, "tree_search_entry") else "").strip().lower()
        if not q:
            self._set_status("请输入搜索关键字")
            return
        matches = []
        # 遍历所有节点，匹配键名或显示的值
        for item_id, path_list in self.item_paths.items():
            text = self.tree.item(item_id, "text") or ""
            vals = self.tree.item(item_id, "values") or ("", "")
            val_display = " ".join(str(v) for v in vals)
            if q in str(text).lower() or q in val_display.lower():
                matches.append(item_id)
        if not matches:
            self._set_status("未找到匹配项")
            return
        # 展开所有匹配项的祖先并选中第一个
        for m in matches:
            parent = self.tree.parent(m)
            while parent:
                self.tree.item(parent, open=True)
                parent = self.tree.parent(parent)
        first = matches[0]
        self.tree.selection_set(first)
        self.tree.see(first)
        self._set_status(f"匹配 {len(matches)} 项")

    def _get_value_by_path(self, path):
        cur = self.data
        for p in path:
            if isinstance(cur, dict):
                cur = cur.get(p)
            elif isinstance(cur, list) and isinstance(p, int):
                if 0 <= p < len(cur):
                    cur = cur[p]
                else:
                    return None
            else:
                return None
        return cur

    def _set_value_by_path(self, path, new_value):
        if not path:
            # root replace
            self.data = new_value
            return True
        # traverse to parent
        parent = self.data
        for p in path[:-1]:
            parent = parent[p] if isinstance(parent, (dict, list)) else None
        key = path[-1]
        if isinstance(parent, dict):
            parent[key] = new_value
            return True
        elif isinstance(parent, list) and isinstance(key, int):
            if 0 <= key < len(parent):
                parent[key] = new_value
                return True
        return False

    def apply_value_change(self):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0])
        txt = self.value_text.get("1.0", tk.END)
        try:
            # parse as YAML fragment
            new_val = yaml.safe_load(txt)
        except Exception as e:
            messagebox.showerror("更新失败", f"值不是有效的 YAML 片段:\n{e}")
            return
        ok = self._set_value_by_path(path, new_val)
        if ok:
            self.unsaved = True
            self._refresh_views()
            self._set_status("已更新选中项，尚未保存")
        else:
            messagebox.showerror("更新失败", "无法更新指定路径的值")

    def add_item(self):
        sel = self.tree.selection()
        if not sel:
            messagebox.showinfo("提示", "请选择要添加到的父节点（字典或列表）")
            return
        path = self.item_paths.get(sel[0])
        parent = self._get_value_by_path(path)
        if isinstance(parent, dict):
            key = simple_prompt(self, "新键名", "输入要添加的键名：")
            if key is None or key == "":
                return
            val_text = simple_prompt(self, "新键值", "输入值（YAML 片段）：")
            if val_text is None:
                return
            try:
                val = yaml.safe_load(val_text)
            except Exception as e:
                messagebox.showerror("添加失败", f"值不是有效的 YAML:\n{e}")
                return
            parent[key] = val
        elif isinstance(parent, list):
            val_text = simple_prompt(self, "新列表项", "输入列表项（YAML 片段）：")
            if val_text is None:
                return
            try:
                val = yaml.safe_load(val_text)
            except Exception as e:
                messagebox.showerror("添加失败", f"值不是有效的 YAML:\n{e}")
                return
            parent.append(val)
        else:
            messagebox.showerror("添加失败", "仅支持向字典或列表添加内容")
            return
        self.unsaved = True
        self._refresh_views()
        self._set_status("已添加内容，尚未保存")

    def delete_item(self):
        sel = self.tree.selection()
        if not sel:
            return
        node = sel[0]
        path = self.item_paths.get(node)
        if not path:
            return
        if not messagebox.askyesno("确认删除", "确定删除选中项吗？"):
            return
        if not path:
            messagebox.showerror("删除失败", "不支持删除根节点")
            return
        # delete
        parent_path = path[:-1]
        parent = self._get_value_by_path(parent_path)
        key = path[-1]
        if isinstance(parent, dict):
            parent.pop(key, None)
        elif isinstance(parent, list) and isinstance(key, int):
            if 0 <= key < len(parent):
                parent.pop(key)
        else:
            messagebox.showerror("删除失败", "无法删除该类型")
            return
        self.unsaved = True
        self._refresh_views()
        self._set_status("已删除内容，尚未保存")

    def rename_key(self):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0])
        if not path or len(path) == 0:
            return
        parent = self._get_value_by_path(path[:-1])
        old_key = path[-1]
        if not isinstance(parent, dict) or not isinstance(old_key, str):
            messagebox.showinfo("提示", "仅字典键支持重命名")
            return
        new_key = simple_prompt(self, "重命名键", f"将键 '{old_key}' 重命名为：")
        if new_key is None or new_key == "" or new_key == old_key:
            return
        if new_key in parent:
            messagebox.showerror("重命名失败", "目标键已存在")
            return
        parent[new_key] = parent.pop(old_key)
        self.unsaved = True
        self._refresh_views()
        self._set_status("已重命名，尚未保存")

    def duplicate_item(self):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0])
        parent = self._get_value_by_path(path[:-1]) if path else None
        key = path[-1] if path else None
        if isinstance(parent, dict) and isinstance(key, str):
            new_key = simple_prompt(self, "复制键为", f"输入新键名（复制自 '{key}'）：")
            if not new_key:
                return
            if new_key in parent:
                messagebox.showerror("复制失败", "目标键已存在")
                return
            parent[new_key] = copy.deepcopy(parent[key])
        elif isinstance(parent, list) and isinstance(key, int):
            value = self._get_value_by_path(path)
            parent.insert(key + 1, copy.deepcopy(value))
        else:
            messagebox.showinfo("提示", "仅支持复制字典键或列表项")
            return
        self.unsaved = True
        self._refresh_views()
        self._set_status("已复制，尚未保存")

    def move_item(self, delta: int):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0])
        parent = self._get_value_by_path(path[:-1]) if path else None
        key = path[-1] if path else None
        if not (isinstance(parent, list) and isinstance(key, int)):
            messagebox.showinfo("提示", "仅列表项支持移动")
            return
        new_index = key + delta
        if new_index < 0 or new_index >= len(parent):
            return
        item = parent.pop(key)
        parent.insert(new_index, item)
        self.unsaved = True
        self._refresh_views()
        self._set_status("已移动，尚未保存")

    def edit_value_quick(self):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0])
        value = self._get_value_by_path(path)
        try:
            init_txt = yaml.safe_dump(value, allow_unicode=True, sort_keys=False)
        except Exception:
            init_txt = str(value)
        txt = multiline_prompt(self, "编辑值（YAML 片段）", init_txt)
        if txt is None:
            return
        try:
            new_val = yaml.safe_load(txt)
        except Exception as e:
            messagebox.showerror("更新失败", f"值不是有效的 YAML 片段:\n{e}")
            return
        ok = self._set_value_by_path(path, new_val)
        if ok:
            self.unsaved = True
            self._refresh_views()
            self._set_status("已更新选中项，尚未保存")

    def copy_path(self):
        sel = self.tree.selection()
        if not sel:
            return
        path = self.item_paths.get(sel[0], [])
        p = self._format_path(path)
        try:
            self.clipboard_clear()
            self.clipboard_append(p)
            self._set_status(f"已复制路径: {p}")
        except Exception:
            pass

    def _format_path(self, path):
        out = []
        for k in path:
            if isinstance(k, int):
                out.append(f"[{k}]")
            else:
                if out and not out[-1].startswith("["):
                    out.append(".")
                out.append(str(k))
        # join while respecting brackets
        s = "".join(out)
        # normalize leading dot
        return s.lstrip(".")

    def validate_yaml(self):
        try:
            txt = self.raw_text.get("1.0", tk.END)
            yaml.safe_load(txt)
            messagebox.showinfo("验证成功", "YAML 语法有效")
        except Exception as e:
            messagebox.showerror("验证失败", f"语法错误:\n{e}")

    def format_yaml(self):
        try:
            txt = self.raw_text.get("1.0", tk.END)
            data = yaml.safe_load(txt)
            formatted = yaml.safe_dump(data, allow_unicode=True, sort_keys=False)
            self.raw_text.delete("1.0", tk.END)
            self.raw_text.insert("1.0", formatted)
            self.data = data
            self.unsaved = True
            self._set_status("已格式化，尚未保存")
        except Exception as e:
            messagebox.showerror("格式化失败", f"无法格式化:\n{e}")

    def _set_status(self, text: str):
        suffix = " • 未保存" if self.unsaved else ""
        path = f" | {self.current_file}" if self.current_file else ""
        self.status.config(text=f"{text}{path}{suffix}")

    # 最近目录持久化
    def _load_recent_dirs(self) -> list[str]:
        try:
            if RECENTS_FILE.exists():
                return json.loads(RECENTS_FILE.read_text(encoding="utf-8"))
        except Exception:
            pass
        return []

    def _save_recent_dirs(self):
        try:
            RECENTS_FILE.write_text(json.dumps(self.recent_dirs[:10], ensure_ascii=False, indent=2), encoding="utf-8")
        except Exception:
            pass

    def _add_recent_dir(self, path: Path):
        s = str(path)
        # 去重并置顶
        self.recent_dirs = [x for x in self.recent_dirs if x != s]
        self.recent_dirs.insert(0, s)
        self.recent_dirs = self.recent_dirs[:10]
        self._save_recent_dirs()

    def _update_recent_menu(self):
        try:
            self.recent_menu.delete(0, tk.END)
        except Exception:
            return
        if not self.recent_dirs:
            self.recent_menu.add_command(label="(暂无记录)", state="disabled")
            return
        for d in self.recent_dirs:
            self.recent_menu.add_command(label=d, command=lambda p=d: self._open_recent_dir(p))
        self.recent_menu.add_separator()
        self.recent_menu.add_command(label="清空最近目录", command=self._clear_recent_dirs)

    def _open_recent_dir(self, path_str: str):
        p = Path(path_str)
        if p.exists() and p.is_dir():
            self.working_dir = p
            self._refresh_file_list()
            self._start_watch_dir()
        else:
            messagebox.showerror("打开失败", f"目录不可用: {path_str}")

    def _clear_recent_dirs(self):
        self.recent_dirs = []
        try:
            if RECENTS_FILE.exists():
                RECENTS_FILE.unlink()
        except Exception:
            pass
        self._update_recent_menu()

    # 目录变动自动刷新
    def _compute_dir_signature(self) -> str:
        if not (self.working_dir and self.working_dir.exists()):
            return ""
        try:
            paths = list(self.working_dir.rglob("*.yaml")) + list(self.working_dir.rglob("*.yml"))
            files = [p for p in paths if p.is_file()]
            meta = [(str(p), int(p.stat().st_mtime_ns), p.stat().st_size) for p in files]
            meta.sort()
            return json.dumps(meta, ensure_ascii=False)
        except Exception:
            return ""

    def _start_watch_dir(self):
        self._dir_signature = self._compute_dir_signature()
        self._schedule_watch_tick()

    def _schedule_watch_tick(self):
        try:
            if self._watch_after_id:
                self.after_cancel(self._watch_after_id)
        except Exception:
            pass
        self._watch_after_id = self.after(2000, self._watch_dir_tick)

    def _watch_dir_tick(self):
        try:
            new_sig = self._compute_dir_signature()
            if new_sig != self._dir_signature:
                self._dir_signature = new_sig
                self._refresh_file_list()
                self._set_status("检测到目录变动，已刷新")
        except Exception:
            pass
        finally:
            self._schedule_watch_tick()


class SimplePromptDialog(tk.Toplevel):
    def __init__(self, master, title: str, label: str):
        super().__init__(master)
        self.title(title)
        self.resizable(False, False)
        self.value = None
        ttk.Label(self, text=label).pack(padx=12, pady=(12, 4))
        self.entry = ttk.Entry(self, width=48)
        self.entry.pack(padx=12, pady=4)
        btns = ttk.Frame(self)
        btns.pack(padx=12, pady=8, fill=tk.X)
        ttk.Button(btns, text="确定", command=self._ok).pack(side=tk.LEFT)
        ttk.Button(btns, text="取消", command=self._cancel).pack(side=tk.LEFT, padx=8)
        self.bind("<Return>", lambda e: self._ok())
        self.bind("<Escape>", lambda e: self._cancel())
        self.transient(master)
        self.grab_set()
        self.entry.focus_set()

    def _ok(self):
        self.value = self.entry.get()
        self.destroy()

    def _cancel(self):
        self.value = None
        self.destroy()


def simple_prompt(master, title: str, label: str):
    dlg = SimplePromptDialog(master, title, label)
    master.wait_window(dlg)
    return dlg.value


class MultiLinePromptDialog(tk.Toplevel):
    def __init__(self, master, title: str, initial: str = ""):
        super().__init__(master)
        self.title(title)
        self.resizable(True, True)
        self.value = None
        ttk.Label(self, text=title).pack(padx=12, pady=(12, 4), anchor="w")
        frame = ttk.Frame(self)
        frame.pack(padx=12, pady=4, fill=tk.BOTH, expand=True)
        self.text = tk.Text(frame, wrap="none")
        ys = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.text.yview)
        xs = ttk.Scrollbar(frame, orient=tk.HORIZONTAL, command=self.text.xview)
        self.text.configure(yscrollcommand=ys.set, xscrollcommand=xs.set)
        self.text.pack(fill=tk.BOTH, expand=True)
        ys.pack(side=tk.RIGHT, fill=tk.Y)
        xs.pack(side=tk.BOTTOM, fill=tk.X)
        self.text.insert("1.0", initial or "")
        btns = ttk.Frame(self)
        btns.pack(padx=12, pady=8, fill=tk.X)
        ttk.Button(btns, text="确定", command=self._ok).pack(side=tk.LEFT)
        ttk.Button(btns, text="取消", command=self._cancel).pack(side=tk.LEFT, padx=8)
        self.bind("<Control-Return>", lambda e: self._ok())
        self.bind("<Escape>", lambda e: self._cancel())
        self.transient(master)
        self.grab_set()

    def _ok(self):
        self.value = self.text.get("1.0", tk.END)
        self.destroy()

    def _cancel(self):
        self.value = None
        self.destroy()


def multiline_prompt(master, title: str, initial: str = ""):
    dlg = MultiLinePromptDialog(master, title, initial)
    master.wait_window(dlg)
    return dlg.value


def main():
    app = YamlEditorApp()
    app.mainloop()


if __name__ == "__main__":
    main()